Skip to content

feat(adr-0087): runtime-export-retired reads literal subpath keys - #33520

Queued
objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/project-thread-l4k5n6
Queued

objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/project-thread-l4k5n6

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #30598

domain:skills#1 · os-project-manager · session_01FEkQ5kVi3STvGvJ9hHHAgS

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:ADR-0087 门禁的 runtime-export-retired 类别现在认包 exports 映射里的字面子路径键:包/子路径#导出、包/子路径#类型.成员、包/子路径#*(该键在 HEAD 离开映射、包还在)。子路径入口按 dist/X.d.ts 对应唯一的 src/X.ts 或 src/X.tsx 读;通配键、非 TS 键(./style.css)、对不上源文件的键都按名拒绝。ADR-0087 D9 原地加一条修订,自测加 SP 电池 80 例。
  • 为什么改:以前只读根入口,子路径上退役的导出只能写 no-migration-prescription 并去掉箭头躲检测器,正是 D7 要消除的形态。你在决策卡上选了「A 收窄:只认字面子路径键」(卡上记录 6885400963)。
  • 风险与回滚:只动门禁脚本和 ADR 文字,不发布任何包;旧的根入口三种写法行为不变(原有 739 个自测断言全绿)。回滚即还原本 PR。
  • 先看:scripts/check-adr-0087-registration.mjs 的 subpathSourceEntry 与 verifySubpathRetired;parseExportOperand;ADR-0087 D9 新增的 2026-11-14 修订段。
  • 两分钟验证:node scripts/check-adr-0087-registration.mjs --self-test,预期 ✓ ... 819 assertions。
  • 需要你做的:审阅并合并(ADR 属 Tier H,需你亲手合并或授权批准)。

What changes

scripts/check-adr-0087-registration.mjs, category runtime-export-retired.

  • parseExportOperand splits the operand at the package name (splitPackageSpecifier): @scope/pkg/a/b#X reads the ./a/b key, @scope/pkg#X the . key as before. It returns subpath and specifier beside the old fields. A subpath with a * anywhere is refused by name as a WILDCARD; an empty, . or .. segment is refused; a subpath spelling a TS source file keeps the old "names a FILE" refusal.
  • sourceEntryAt takes the key. The root branch is unchanged. A subpath key (subpathSourceEntry) reads its types entry with resolveTypesEntry(manifest, './sub') and maps dist/P.d.ts (or a .d.mts / .d.cts twin) to exactly one of src/P.ts / src/P.tsx. Refused by name: a key only a wildcard key reaches (wildcardKeysMatching, which never resolves the pattern), a key that names no .d.ts (./style.css), an unmappable key, an ambiguous key, and an absent key (marked keyAbsent).
  • Export and member operands on a subpath run the three predicates unchanged, through a surface cache keyed by rev, package and key. When the key left the map at HEAD, the refusal names the whole-key spelling.
  • PACKAGE/SUB#* is new (verifySubpathRetired). Predicate 1: the key maps at the base; at HEAD the package is still a workspace member (a deleted one points at PACKAGE#*), it declares an exports map (a missing field and "exports": null are both refused: Node and TypeScript read null as absent), the key is gone and no wildcard key reaches it. Predicate 2: none of the package's own files the key's entry reaches is a metadata surface (a file of another workspace package the entry re-exports is that package's, judged when its name is an export operand), and no metadata surface outside the package names the specifier in code (packageReference, shared with PACKAGE#*). Predicate 3: one FROM line names the specifier.
  • Header comments, the HOW hint and the "deliberately NOT checked" paragraph are updated. New battery SP (#30598), floor 80.

docs/adr/0087-metadata-protocol-upgrade-contract.md, D9, plus one pointer in the 2026-10-12 addendum's opening. A third amendment note under the unmappable-entry paragraph, in the shape of the two 2026-10-27 notes (provenance, measurement, the three marker forms, the refusals, the three predicates for #*). The "What this does not decide" sentence now names the wildcard refusal. The Status line gets an Amended entry, and the 2026-10-12 addendum's opening sentence points at the new note.

Why

Ruling: the maintainer chose A, narrowed, 「A 收窄:只认字面子路径键」, on a decision card at 2026-11-14T22:42Z; the option read 「新增 <pkg>/<sub>#Export、#Type.member、#*;通配键按名拒绝。改门禁并修订 D9,立一张 skills 卡。」 Recorded on the card as comment 6885400963. Analysis: project file pm-orders/skills/30598-decision-analysis.md.

Acceptance notes

Verification

Changed: 2 files. dispatch-gates --tier: MANDATORY contract-review tier (docs/adr/**, no one-line exit). check-governed-merges --branch: GOVERNED, Tier H (docs/adr/** ×1), 508 changed lines, under 3000. Changeset: none, labelled skip-changeset: the diff is docs/adr/** and a repo-root scripts/ gate, and no package's files[] ships either.

Local gates at head 6d1f3bce (round 3, after the at-tier contract review 6886775383; round 2 was dc3d60c0 merged with origin/main e3f0cfd4 as 92259ce2). The derived list was taken and run on 6d1f3bce itself; earlier rounds ran the same 44 families on dd6bc2a7 and 92259ce2, all exit 0.

44 derived gate families on 6d1f3bc, all exit 0; `--ran` reconciliation 44 / 44, 0 NOT-MEASURED

Closure built first: pnpm --filter "@objectstack/lint..." build under the verify lock, VERDICT command-exit 0.

node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm check:adr-anchors :: exit 0
pnpm check:adr-index :: exit 0
pnpm check:adr-symbol-anchors :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:future-spec-major :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:scripts-symbol-anchors :: exit 0
pnpm check:watch-hint-literal :: exit 0
The gate's own self-test: 819 assertions

With the new code and before the new battery was added, the self-test printed 739 assertions, every existing battery green (the root operands are unchanged). With battery SP (#30598) it prints ✓ check-adr-0087-registration --self-test: 819 assertions over real temp git repos. SP registers 80 cases: G1 to G6 (export, member, whole key, root and subpath in one marker, arrow form, whole key over an entry that re-exports another package's root), R1 to R20 (wildcard reach, wildcard spelling, non-TS key, unmappable key, absent key, still exported, export on a gone key, key still in the map, wildcard at HEAD, package deleted, no exports map at HEAD, metadata surface importing the subpath, entry reaching a metadata surface, per-export FROM lines, ambiguous .ts/.tsx, "exports": null at HEAD, no whole-key hint without an exports map, a types condition naming a TS source, a member hint naming the subpath spelling, the re-exported name refused as an export operand; SP-R17c/d pin a missing exports field beside the null case), and unit pins U1 to U9 on the parser, splitPackageSpecifier and wildcardKeysMatching. One-off mutations, each restored and never committed: the null check back to an undefined-only test reds SP-R16; dropping the own-directory filter on the entry's files reds SP-G6. The gate has no separate *.test.ts; git grep of its file name in test files has no hit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5kVi3STvGvJ9hHHAgS

The category's three operands take an optional LITERAL key of the package's
`exports` map: `<package>/<subpath>#<Export>`, `#<Type>.<member>`, and `#*`
for a key that left the map at HEAD while the package stays. The key's entry
maps `dist/<p>.d.ts` (or a twin) to exactly one of `src/<p>.ts|tsx`. A
wildcard key, a non-TypeScript key and an unmappable key are refused by name.
ADR-0087 D9 is amended in place; the SP self-test battery pins each form and
each refusal.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5kVi3STvGvJ9hHHAgS
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 27e5e2e8b4625479d0b50e60d546410ea41ba077
Local-runs: none
Scope: docs/adr/0087-metadata-protocol-upgrade-contract.md + scripts/check-adr-0087-registration.mjs

Inputs read: card #30598 (body, triage comment 6684725634, ruling record 6885400963, claim 6885439323, os-dev-report 6885846856), PR #33520 (body, two-file list, net diff against main e40cd1e2, which is the merge base), and the check-runs on this head. Code outside the diff was read through git show on origin/main and the head, never reviewed: resolveTypesEntry in scripts/check-published-readme-exports.mjs, specifierNamesModule, metadataSurfaceReference, packageReference, readFromBlock and canonicalFromOperand in the gate. Nothing was built, tested or re-run. The record shape was printed by record-recognisers --template --paths, which reads nothing over the network. The measurement below was re-derived by reading manifests and source paths with git show; no repo code was run for it.

Check-runs at render time: all 55 on this head completed, 29 success and 26 skipped, with no failure. The gate's own --self-test ran green in Lint · repo & tooling gates (through check:changeset-gate-self-tests). Check Changeset skipped its ADR-0087 step under skip-changeset, which is expected because no changeset is declared.

① Derived judgments

  • Accept set of runtime-export-retired grows by three operand forms, as ruled. PACKAGE/SUBPATH#Export, PACKAGE/SUBPATH#Type.member and PACKAGE/SUBPATH#* now parse (parseExportOperand through splitPackageSpecifier) and are judged. Before this PR every one was refused at parse time as "not a reference". Right: this is ruling A narrowed, record 6885400963, word for word.
  • Refusals added, each by name. These operands are refused: one that spells a * in its subpath (at parse); one that only a wildcard key reaches (at the base or HEAD read); a key whose entry is not a .d.ts (./style.css, ./openapi.json); a key that cannot be mapped; a key that maps to both .ts and .tsx; a key the map does not have (keyAbsent, whose HEAD refusal names the PACKAGE/SUBPATH#* spelling). For #: the key still in the HEAD map, a wildcard key still reaching it, the package deleted at HEAD (the refusal points to PACKAGE#), no exports map at HEAD, the key's entry reaching a metadata surface, and a metadata surface outside the package naming the specifier in code. Right, and each refusal is pinned by battery SP: R1 to R15 and U1 to U9.
  • No wildcard support was added. wildcardKeysMatching is called in exactly two places, subpathSourceEntry and verifySubpathRetired, and both use it only to refuse. No pattern is ever resolved to a target. Right: the scope does not go beyond the ruling.
  • Root operands are unchanged. sourceEntryAt's . branch is the old body, with the declared read moved below the dir computation and no other change. For a root operand, specifier equals the package name, so spelling for PACKAGE#* is still the package name, and the wholeNames switch from o.pkg to o.spelling makes no difference. The surface cache key gains ' .'. keyAbsent is set only on the subpath branch, so the deleted-package hint fires exactly as before. The FILE refusal for a TS path keeps its text. One parse path changed: a repo path with no TS extension (packages/x/src/y.js#X) used to be refused at parse. It now parses as a subpath of a package named packages and is refused at predicate 1 ("no workspace manifest … named packages"). It is still refused. Right.
  • The gate's own export surface grows. It now exports splitPackageSpecifier, wildcardKeysMatching and sourceEntryAt, and the return shape of parseExportOperand gains subpath and specifier. git grep on the head finds no importer of the gate outside the gate itself; it is wired only through package.json scripts and the workflows. Right, with no consumer to break.
  • Self-test. The new battery SP (#30598) is registered in SELF_TEST_BATTERIES with floor 61 and has the five green cases G1 to G5. Right.
  • The ADR's measurement holds. Re-derived from the manifests on main (e40cd1e2) over the pnpm-workspace.yaml globs: 110 published packages and 78 subpath keys other than ./package.json. Of those, 68 map, 4 are unmappable (@objectstack/scenarios ./score, ./score/full, ./runner, ./export), 5 are not TS (four ./style.css and @objectstack/spec ./openapi.json), 1 is a wildcard (@object-ui/i18n ./locales/*) and 0 are ambiguous. That matches the ADR's numbers. 24 packages have a non-./package.json subpath key; the ADR's 26 also counts packages whose only subpath key is ./package.json, which is consistent. The claim about the @objectstack/cli/console changeset is borne out by its no-migration-prescription marker in packages/cli/CHANGELOG.md, which names "ten names … on a published subpath".

② Semver level

No changeset, labelled skip-changeset. The diff is docs/adr/** plus a gate at the repo-root scripts/, and no package's files[] ships either one. Nothing publishes, so the label is right. The PR body's Clause-②: no is right as well: no published schema, type or runtime surface changes. The #30511 changeset (PR #30902) is not touched and not re-graded, as the ruling says.

③ Boundary flags

  • open_questions: empty.
  • Dev deviation, model tier: the thread ran at the claim's default tier, and dispatch-gates --tier reads the contract-review tier for docs/adr/**. This record is the at-tier review that tier owes. Answered.
  • Dev deviation, PR-body signature: an os-dev.md rule against the standing lines. This is process, not contract. Not judged here.
  • Dev deviation, the order's third surface is empty: git grep runtime-export-retired on the head finds only the ADR, the gate, three pin tests and one index.ts under packages/. No skill, .claude/**, content/** or AGENTS.md teaches the operand grammar. Confirmed. The gate's HOW hint names the subpath form.
  • Dev deviation, serial PR docs(adr-0087): the disposition text names the second no-migration-prescription refusal #33460: still open. Its two hunks edit ADR-0087 near lines 762 and 864 (the 2026-08-13 addendum). They do not touch the Status line or D9, so there is no textual overlap with this diff. The landing order is the seat's call.
  • Out-of-scope finding: a re-export of ANOTHER package's subpath is still not followed, and it stays an unresolved star that refuses an absence claim. That is the conservative direction, and the gate's "deliberately NOT checked" paragraph now says so. Accepted as is.

④ Design judgment

docs/adr/0087-metadata-protocol-upgrade-contract.md (D9, the amendment note under the unmappable-entry paragraph, "What this does not decide", the Status line). Checked against its first consumer, the gate in this diff, and against the ruling as its execution plan. The note has the same shape as the two earlier D9 amendments: provenance, measurement, the three marker forms, the refusal list, and the three predicates for #. Every sentence has a matching branch in the gate: the resolveTypesEntry(manifest, './SUBPATH') read, the mapping from dist/P.d.ts (and its twins) to exactly one of src/P.ts or src/P.tsx, the four refusals, the HEAD refusal naming the whole-key spelling, and the # predicates (base mapping; at HEAD a workspace member with an exports map, the key absent and no wildcard reaching it; the files the entry reaches; outside surfaces naming the specifier; one FROM line naming the specifier). The claim "an import of the subpath already counts as an import of the package's module" is true: specifierNamesModule accepts owner/ prefixes. The scope stops where the ruling stops: "A wildcard key is not resolved."

  • nit: The opening paragraph of the 2026-10-12 addendum (head line 1040) still describes "three operand kinds" in root-only spelling and points only to the second 2026-10-27 note. One clause pointing to the 2026-11-14 note would let a reader who starts there find the subpath form. This is a prose face and can be fixed in place. It lies outside the order's "D9 only" surface, so it may also stay as an accepted deviation.

scripts/check-adr-0087-registration.mjs (implementing file). Checked sentence by sentence against D9 as amended, and probed with the five attacks the card names:

  • A wildcard key that slips through: none. A * anywhere in the subpath is refused at parse. At the base and at HEAD, a key reached only through a pattern gives declared: false from resolveTypesEntry, which means a literal lookup, and then a wildcard refusal. A literal key beside a pattern is read literally, which is Node's precedence. For #*, any matching pattern at HEAD refuses.
  • An unmappable or non-TS key that is not refused by name: none. Each of these is refused by name: an entry that is not .d.ts, a types entry outside dist/ (the stem is null), a stem with no source file, and a stem with both source files.
  • PACKAGE/SUBPATH# passing while the key is still in the HEAD map:* not possible for an object map, because of hasOwnProperty. A string, array or conditions-only exports at HEAD correctly counts as declaring only .. See the should-fix finding below for exports: null.
  • Predicate 3's spelling for the new operands: right. A whole key's spelling is the specifier, canonicalFromOperand returns null for a specifier, and the wholeNames fallback reads it exactly. SP-G3, G5 and R14 pin this.
  • Root operands: unchanged; see ①.

Findings:

  • should-fix — predicate 1 for PACKAGE/SUBPATH# admits a HEAD manifest whose exports is null.* verifySubpathRetired (head line 5113) refuses only map === undefined. With "exports": null the check map && typeof map === 'object' is false, wildcardKeysMatching(null) returns nothing, and the claim passes. Node (packageConfig.exports != null) and TypeScript both treat a null exports as absent, so every file is importable by path. That is exactly the case the ADR's sentence excludes: "it declares an exports map (without one every file is importable by path, so no key's absence proves anything)". No manifest on main writes null, but the predicate admits a claim the contract says it refuses. Fix: test map == null (and say "null or absent" in the refusal), and pin it with one red case beside SP-R11. The keyAbsent wording at head line 4990 should read == null the same way.
  • nit — the member refusal for a type that left the entry names the root spelling. At head line 5461 the hint "named as an export (${op.pkg}#${op.exportName})" ignores the subpath. For a member operand of PACKAGE/SUBPATH#Type, it points to PACKAGE#Type, which reads the wrong entry. The verdict is still a refusal; only the guidance is wrong. It should use op.specifier. Fixing it moves the head, so either fold it into the should-fix delta or leave it as a follow-up.
  • nit — the non-TS refusal wording covers more cases than it describes. At head line 4996, a key whose types entry is a TS source (./src/x.ts, which resolveTypesEntry's walk does not accept) is refused as "ships no .d.ts … nothing on it is a runtime export". It is refused, which is correct, but the reason given is wrong. No such key exists on main. Accepted deviation, or fold into the delta.
  • nit — the keyAbsent HEAD hint recommends PACKAGE/SUBPATH# even when the HEAD manifest has no exports map,* where verifySubpathRetired will then refuse that spelling. It is refused either way. Accepted deviation, or fold into the delta.
  • nit — wildcardKeysMatching lets * match the empty string (head line 4913 tests that the length is at least pre.length + post.length, while Node requires at least key.length). A literal key at HEAD whose value is null also counts as "still present". Both errors only over-refuse, which is the conservative direction, and the ADR does not pin either one. Accepted deviation.

Implemented-by: claude/project-thread-l4k5n6
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

One should-fix (the exports: null limb of predicate 1 for PACKAGE/SUBPATH#*) earns a delta review of its fix. The four nits do not block. The ADR nit may be fixed in place, and the gate nits may ride along with the should-fix delta or stay as accepted deviations.

Adopted by the seat 2026-11-14T23:38Z · isolated sub-agent at CONTRACT_REVIEW_TIER · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

`"exports": null` reads as absent to Node and TypeScript, so every file is
importable by path and a key's absence proves nothing: the whole-key form now
refuses it as it refuses a missing field. Three messages corrected: a member
whose type left a subpath entry names the subpath export spelling, a key whose
types condition is a TS source file is no declaration entry, and a package with
no exports map at HEAD is no longer pointed at the whole-key spelling.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5kVi3STvGvJ9hHHAgS
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 92259ce2a8eabb77b335b547bba8c9348cdac80c
Local-runs: none
Scope: docs/adr/0087-metadata-protocol-upgrade-contract.md + scripts/check-adr-0087-registration.mjs

Delta review. The earlier record is PR #33520 comment 6886012642, a PASS on head 27e5e2e8b4625479d0b50e60d546410ea41ba077. It had one should-fix (S1, predicate 1 for PACKAGE/SUBPATH#* admitting a HEAD "exports": null) and five nits: N1 the ADR addendum pointer, N2 the member refusal's root spelling, N3 the non-TS refusal wording, N4 the keyAbsent HEAD hint, N5 wildcardKeysMatching (empty match and a null-valued literal). This record judges fix commit dc3d60c and the merge 92259ce on top of it.

Inputs: comment 6886012642; card #30598 (body; triage 6684725634; ruling record 6885400963, 「A 收窄:只认字面子路径键」; claim 6885439323; the two os-dev-reports 6885846856 and 6886302050, which were listed but not taken as evidence); PR #33520 (body, two-file list, skip-changeset); git diff 27e5e2e8b4625479d0b50e60d546410ea41ba077 dc3d60c0 (one file, scripts/check-adr-0087-registration.mjs, +30 / -8, every line read); the merge 92259ce; and the check-runs on this head. Code outside the delta was read with git show: resolveTypesEntry in scripts/check-published-readme-exports.mjs, readExportSurface, the red / assert / registerCase self-test helpers and the SP fixture. Nothing was built, run or re-run. The record shape follows contractReviewRecordLines in scripts/pm/record-recognisers.mjs at main, which was read and not run.

The merge brings only main. 92259ce merges origin/main e3f0cfd into dc3d60c. Its first-parent diff (24 files, +767 / -76) is byte-identical to git diff e40cd1e2 e3f0cfd4, the three commits main gained since the previous merge base (#33467, #33290, #33448). Neither of the PR's two files is in it. The gate at 92259ce is byte-identical to the gate at dc3d60c, and the ADR is unchanged since 27e5e2e. No conflict resolution touched a PR hunk. Against main the PR is still two files, +493 / -39.

Gate verdicts at render: 43 check-runs exist on this head. 15 are success, 19 skipped and 9 still in progress, with 0 failed. The 9 running are Test Core (1/8), Test Core (2/8), Lint · PM dispatch-gates, Lint · CI, release & engine gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Lint · docs & code guards and Type Check · consumer gates. The gate's own --self-test runs in Lint · docs & code guards (step "Changeset-family gate self-tests", through check:changeset-gate-self-tests). That step was still pending at render, so the new floor of 73 has no CI verdict yet. Lint · repo & tooling gates, Lint · ESLint and Type Check · source gates are success. Check Changeset is skipped, which is the skip-changeset path. This record did not wait for the rest.

Delta

  • S1 (should-fix), "exports": null admitted by predicate 1 for PACKAGE/SUBPATH# · fixed.* verifySubpathRetired (head L5119) now refuses map == null. A null map adds "("exports": null reads as absent)" to the refusal. subpathSourceEntry (L4989) uses the same predicate, manifest.exports == null, for the new noExportsMap flag. I tried to break it at HEAD with five shapes:

    • null and a missing field are both refused, with one test each: SP-R16 and SP-R11.
    • A string or an array is Node's sugar for . alone. resolveTypesEntry answers declared: false for the subpath, no key is present and no wildcard matches, so PACKAGE/SUBPATH#* passes. That is right, because the subpath really is unexported.
    • A conditions-only object and {} also declare . alone or nothing, and they pass for the same reason. That is right.
    • false or a number would pass. Node exports nothing for them either. That is right, and no manifest on main writes one.

    At the base, an Export or member operand on a package whose base exports is null or missing is refused ("cannot be read at the merge base: the exports map … has no ./SUB key … (it declares no exports map at all[: "exports": null reads as absent])"). PACKAGE/SUBPATH#* wraps that same text in "has no published entry this gate can read at the merge base". A string, array or conditions-only base gets the plain "has no ./SUB key", which is true for each one. The base-side wording matches the code in every case.

  • N1 (nit), pointer from the 2026-10-12 addendum's opening paragraph · not fixed. The ADR is unchanged in the delta. Head L1040 still reads "three operand kinds" and points only to the second 2026-10-27 note. It is still a prose face outside the order's D9-only surface, so it stands as an accepted deviation or a fix in place.

  • N2 (nit), member refusal named the root spelling · fixed. L5469 now writes ${op.specifier}#${op.exportName}. For a root operand specifier equals pkg, so root behaviour is unchanged.

  • N3 (nit), non-TS refusal wording · fixed. L4999 now says that none of the key's conditions names a .d.ts / .d.mts / .d.cts file, "a stylesheet, a JSON file or a TS SOURCE file is not one", so there is no declaration entry to map. It no longer claims that nothing on the key is a runtime export. One residual is graded as new nit F3 below.

  • N4 (nit), the keyAbsent HEAD hint pointed at PACKAGE/SUBPATH# with no map at HEAD · fixed.* readExportSurface returns the sourceEntryAt refusal object unchanged, so noExportsMap reaches L5422. With no map, the hint now says that neither the operand nor SPECIFIER#* can prove the retirement. The flag and verifySubpathRetired's refusal use the same == null test, so the hint and the PACKAGE/SUBPATH#* verdict cannot disagree. With a string, array or conditions-only HEAD map the hint still names PACKAGE/SUBPATH#*, which then passes. That is consistent.

  • N5 (nit), wildcardKeysMatching empty match and null-valued literal · not fixed. L4913 still admits an empty * match, and L5126 still counts a literal key whose value is null as present. Both only refuse more, never less. They stay as the accepted deviation the earlier record allowed.

The new self-test cases. SP's floor went from 61 to 73. Each assert registers one case: R16 adds 3, R17 adds 3, R17b adds 1, R18 adds 3 and R19 adds 2. The total is 12, so the floor equals the count. Here is what happens to each case if the fix is reverted:

  • SP-R16 (#*, HEAD exports: null) would go green under map === undefined. Its fixture differs from the green SP-G3 only in that manifest, so "expected RED, got green" fails, and so does the null-suffix pattern. It pins S1.
  • SP-R17 / R17b (Export operand, HEAD exports: null) would still be red under the old hint. But the "neither this operand nor … can prove" pattern would fail, and R17b's guard against the "LEFT the exports map" hint would fail too. They pin N4.
  • SP-R18 (types: './src/raw.ts') would still be red under the old text, but the "a TS SOURCE file is not one" pattern would fail. It pins N3.
  • SP-R19 (member operand, TYPE gone from the subpath entry) would still be red under the old text, but the /templates#TemplateOptions pattern would fail against plugin-email#TemplateOptions. It pins N2.

All four would fail on a revert. messages (L8225) is declared at selfTest scope, so R17b can reach it from inside the SP block.

① Derived judgments

  • The accept set does not grow in the delta. The only change to a verdict is one more refusal: PACKAGE/SUBPATH#* with a HEAD exports: null, which every earlier head admitted. The other three edits change message text only (N2, N3, N4). No operand that was refused before is now admitted.
  • The gate's export surface is unchanged. subpathSourceEntry is not exported. The only new field on its refusal object is noExportsMap, and the gate itself is its only reader.
  • Root operands are unchanged. sourceEntryAt's . branch is not touched. N2's op.specifier equals op.pkg for a root operand. The new hint branch needs keyAbsent, and only the subpath branch sets it.

② Semver level

Unchanged from the earlier record: there is no changeset, and skip-changeset is right. The delta touches only the repo-root gate, which no package's files[] ships. Clause-②: no stands.

③ Boundary flags

④ Design judgment

docs/adr/0087-metadata-protocol-upgrade-contract.md. It is unchanged since 27e5e2e and was re-checked against the ruling and against the gate as fixed.

  • The Status line's Amended (2026-11-14, check-adr-0087(runtime-export-retired): the operand names only a package's root entry, so a retired SUBPATH export (./locales/zh → ./locales/zh-Hans) can only be held as no-migration-prescription #30598) entry cites decision A narrowed, record 6885400963, literal subpath keys only, with a wildcard refused by name. That is the ruling's option text.
  • D9's 2026-11-14 note lists the three operand forms, the refusals and the three #* predicates. Predicate 1's "it declares an exports map (without one every file is importable by path …)" now matches the gate for null as well, because a null is no map, so the prose needs no change.
  • "A key the map does not have. At HEAD that refusal names the whole-key spelling" is still literally true when HEAD has no map, because the new hint names SPECIFIER#* while saying it cannot prove the retirement.
  • The "What this does not decide" sentence still matches: a wildcard key is refused by name and never resolved.
  • One residual from the gate fix is new nit F1.

scripts/check-adr-0087-registration.mjs. S1, N2, N3 and N4 hold, as set out under Delta.

New findings:

  • nit (F1), the ADR's non-TS bullet keeps the rationale the gate just corrected. D9's note says a key whose entry is not a TypeScript declaration "exports no name a TypeScript consumer imports". That is true for ./style.css, its example. It is false for a key whose types condition names a TS source file, which the gate now refuses (SP-R18) for the honest reason: no declaration entry to map. It is a prose face and can be fixed in place together with N1, or accepted.
  • nit (F2), the undefined limb of the N4 hint is not pinned. SP-R17 drives only exports: null. A mutation of L4989 to manifest.exports === null would bring back N4 exactly for a missing exports field, and the self-test would stay green. SP-R11 pins the missing field only on PACKAGE/SUBPATH#*. The fix would be one more red like R17 with exports: undefined. Accepted deviation, or the next touch.
  • nit (F3), the new non-TS wording claims more than the walk reads. "None of its conditions names a .d.ts" is only as wide as resolveTypesEntry's walk, which reads types, import, module, node, default and require. A key with its .d.ts only under another condition (browser, development) would be refused with a sentence that is false for it. It is still refused. The earlier record's measurement found no such key on main. Accepted deviation.
  • nit (F4), the PR body is stale for this head. It still says floor 61, 800 assertions, R1 to R15, and local gates on 27e5e2e, and it does not mention the null refusal. This is a body edit, not a code change. It does not move the head.
  • nit (F5), the jsdoc @returns of sourceEntryAt (L4937) lists keyAbsent but not noExportsMap. Cosmetic.

Implemented-by: claude/project-thread-l4k5n6
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

The should-fix S1 is fixed and pinned. N2, N3 and N4 are fixed and pinned. N1 and N5 stand as accepted deviations. The five new findings are all nits, so this round ends. Nine check-runs, among them the lane that runs this gate's --self-test, were still in progress at render with no failure so far. Their conclusions are the gate verdicts the landing reads.

Adopted by the seat 2026-11-15T00:17Z · isolated sub-agent at CONTRACT_REVIEW_TIER · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

维护者速读

  • 改了什么:ADR-0087 门禁 runtime-export-retired 新增三种字面子路径操作数:包/子路径#导出、包/子路径#类型.成员、包/子路径#*(该键在 HEAD 的 exports 映射里已删除)。通配键、映射不到 src/ 的键、非 TS 的键都按名拒绝。ADR-0087 D9 同步原位修订,并改了「What this does not decide」一句。
  • 为什么改:执行你在决策卡上选的「A 收窄:只认字面子路径键」(卡 check-adr-0087(runtime-export-retired): the operand names only a package's root entry, so a retired SUBPATH export (./locales/zh → ./locales/zh-Hans) can only be held as no-migration-prescription #30598)。
  • 风险与回滚:只动仓库根 scripts/ 下的门禁和一篇 ADR,不发布任何包,不改任何已有操作数的判定。回滚即还原本 PR。
  • 先看:ADR-0087 D9 的 2026-11-14 修订注,以及门禁里的 verifySubpathRetired。
  • 两分钟验证:node scripts/check-adr-0087-registration.mjs --self-test,预期 812 条断言全过(SP 下限 73)。
  • 需要你做的:受管面 Tier H(docs/adr/**)。契约复审 PASS 评论 6886012642(含 1 条应修)与增量复审 PASS 评论 6886472906(Head-sha 92259ce2a8eabb77b335b547bba8c9348cdac80c)已在案,请批准或直接合并。
  • 席位意见:可以合。首轮应修(HEAD 的 exports: null 会被当成「该键已删」放过)已改好,并有自测钉住。PR docs(adr-0087): the disposition text names the second no-migration-prescription refusal #33460 也改 ADR-0087,但两者的段落不重叠,谁先合都行。

席位核对

Verdict: ACCEPT, held for the maintainer · head 92259ce2a8eabb77b335b547bba8c9348cdac80c · card #30598 · reports 6885846856 and 6886302050 · contract review 6886012642 (PASS with should-fix on 27e5e2e8) and delta 6886472906 (PASS on this head, CONTRACT_REVIEW_TIER; Ruling-ref for the in-seat review: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb option A).

Implemented-by: claude/project-thread-l4k5n6
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 15, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Correction: contract-review tier

The contract-review record(s) 6886012642, 6886472906 on this PR say Served-tier: CONTRACT_REVIEW_TIER, and the seat adopted them as "isolated sub-agent at CONTRACT_REVIEW_TIER". That is wrong. The seat launched the reviewing sub-agent without the tier, so it ran at the seat's own default tier, not at CONTRACT_REVIEW_TIER. The seat found this on its own audit. The review was still isolated and adversarial, and its findings stand as an off-tier review. They are not the at-tier record AGENTS.md and the in-seat review ruling require.

The seat re-runs the review at CONTRACT_REVIEW_TIER on this PR's head and posts that record here. Until it does, this PR has no at-tier record.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 92259ce2a8eabb77b335b547bba8c9348cdac80c
Local-runs: none
Scope: docs/adr/0087-metadata-protocol-upgrade-contract.md + scripts/check-adr-0087-registration.mjs

At-tier re-review of this head. The two earlier records on this PR (6886012642, 6886472906) were served off-tier and are not evidence here; the tree was judged from scratch, and ③ says which of their findings this record confirms.

Inputs read: card #30598 (body, triage 6684725634, ruling record 6885400963, claim 6885439323, the two os-dev-reports, the ACCEPT note), PR #33520 (body, the two-file list, the net diff against main at merge base e3f0cfd4, +493 / -39), and the check-runs on this head. Code outside the diff was read with git show, never reviewed: resolveTypesEntry in scripts/check-published-readme-exports.mjs, and readExportSurface, packageReference, metadataSurfaceReference, specifierNamesModule, metadataSurfaceKind and readFromBlock in the gate. Nothing was built, tested or re-run. The ADR's measurement was re-derived by reading manifests and file names out of git blobs with a throwaway reader in the scratch space; no repo code ran.

Check-runs on this head: all completed, none failed. Success on every lane that ran, including Lint · docs & code guards (the lane that runs this gate's --self-test, so the SP floor of 73 has a green verdict), Lint · repo & tooling gates, Lint · scoped · tooling self-tests, Type Check · source gates, Test Core (1/8) to (3/8) and Governed Surface Queue Guard. Check Changeset is skipped, which is the skip-changeset path.

① Derived judgments

  • The accept set of runtime-export-retired grows by three operand forms, as ruled. PACKAGE/SUBPATH#Export, PACKAGE/SUBPATH#Type.member and PACKAGE/SUBPATH#* now parse (parseExportOperand through splitPackageSpecifier) and are judged; before, each was refused at parse. Right: this is ruling A narrowed, record 6885400963.
  • No wildcard support. wildcardKeysMatching has two callers, subpathSourceEntry and verifySubpathRetired, and both only refuse. No pattern is expanded to a target anywhere. Right: the scope does not exceed the ruling.
  • Refusals added, each by name. A * in the operand's subpath (parse); a key only a wildcard key reaches (base and HEAD); a key with no .d.ts / .d.mts / .d.cts entry; a types entry outside dist/; a stem with no source; a stem with both .ts and .tsx; a key absent from the map. For the whole-key form: the key still in the HEAD map, a wildcard key reaching it at HEAD, the package gone at HEAD (pointed at PACKAGE#*), no exports map or a null one at HEAD. Right.
  • Root operands are unchanged. sourceEntryAt's . branch is the old body with one line moved below the dir computation. splitPackageSpecifier accepts and refuses the same root spellings PACKAGE_NAME_RE did (a bare scope, an upper-case name and a TS file path are still refused; @scope/pkg.ts is still read as a package name). For a root operand specifier equals pkg, so the wholeNames switch from o.pkg to o.spelling and the op.specifier hint change nothing. keyAbsent is set only on the subpath branch. The one text change a root refusal sees is the extra HOW-hint line. Right.
  • One parse path moved, still refused. A repo path with no TS extension (packages/x/src/y.js#X) used to be refused at parse and is now read as a subpath of a package named packages, refused at predicate 1 as no workspace manifest. Right.
  • The gate's export surface grows (splitPackageSpecifier, wildcardKeysMatching, sourceEntryAt; parseExportOperand returns subpath and specifier). The gate is a repo-root script with no importer outside itself. Right.
  • The ADR's measurement holds. Re-derived on this head: 110 published packages, 78 subpath keys besides ./package.json; 68 map, 4 do not (@objectstack/scenarios ./score, ./score/full, ./runner, ./export), 5 are not TS (four ./style.css, @objectstack/spec ./openapi.json), 1 is a wildcard (@object-ui/i18n ./locales/*), 0 ambiguous. Right. See the nit on "26 of 110" in ④.

② Semver level

No changeset, labelled skip-changeset. The diff is docs/adr/** and a repo-root scripts/ gate; no package's files[] ships either, so nothing publishes and the label is right. Clause-②: no on the PR body is right: no published schema, type or runtime surface changes. The #30511 changeset (PR #30902) is untouched and not re-graded, per the ruling.

③ Boundary flags

  • open_questions: empty in both reports.
  • Dev deviation, model tier: the dev thread ran at the claim's default tier; this record is the at-tier review docs/adr/** owes. Answered.
  • Dev deviation, the order's third surface is empty: no skill or reference teaches the operand grammar; the gate's HOW hint carries the subpath form. Accepted.
  • Dev deviation, PR-body signature and the serial PR docs(adr-0087): the disposition text names the second no-migration-prescription refusal #33460: process and landing order, the seat's call. PR docs(adr-0087): the disposition text names the second no-migration-prescription refusal #33460's hunks sit in the 2026-08-13 addendum, not in the Status line or D9.
  • Out-of-scope finding (a re-export of ANOTHER package's subpath is not followed): confirmed in the tree. resolveModuleNameLiterals asks sourceEntryAt with the whole specifier as a package name, which no manifest carries, so the star stays unresolved and refuses an absence claim. Conservative, and the gate's "deliberately NOT checked" paragraph says so. Accepted.
  • The earlier records' findings, judged on this head:
    • S1 ("exports": null at HEAD admitted by the whole-key form): confirmed as a real defect on the earlier head, and fixed here. verifySubpathRetired tests map == null, subpathSourceEntry uses the same test for noExportsMap, and SP-R16 pins it.
    • N2 (member hint named the root spelling), N3 (non-TS wording), N4 (whole-key hint with no map at HEAD): confirmed and fixed; pinned by SP-R19, SP-R18, SP-R17 and R17b.
    • N1 (the 2026-10-12 addendum's opening still says "three operand kinds" in root spelling), N5 (wildcardKeysMatching lets * match the empty string; a null-valued literal key counts as present), F1, F2, F3, F5: confirmed, still open, all nits; restated in ④.
    • F4 (stale PR body): no longer true. The body on this head says 812 assertions, floor 73, R1 to R19 and gates on 92259ce2.
    • Rejected: the earlier ④ sentence that every ADR sentence has a matching branch in the gate. Predicate 2 for the whole-key form does not; see the should-fix below.

④ Design judgment

docs/adr/0087-metadata-protocol-upgrade-contract.md (Status line; the 2026-11-14 note under D9's unmappable-entry paragraph; "What this does not decide"). Judged against the ruling as its execution plan and the gate in this diff as its first consumer. The note follows the shape of the two 2026-10-27 notes: provenance, measurement, the three marker forms, the refusals, the three predicates for the whole-key form. It stops where the ruling stops: "A wildcard key is not resolved. That waits for a second case." Sentence by sentence against the gate:

  • The export and member forms: resolveTypesEntry(manifest, './SUBPATH'), then dist/P.d.ts or a twin to exactly one of src/P.ts / src/P.tsx. Holds (subpathSourceEntry).
  • "An import of the subpath already counts as an import of the package's module." Holds: specifierNamesModule accepts the owner's name and any owner/ prefix.
  • The four refusal bullets. Hold, each by name.
  • Whole-key predicate 1 (maps at the base; at HEAD a workspace member, an exports map, the key gone, no wildcard reaching it). Holds, limb by limb.
  • Whole-key predicate 3 (one FROM line naming the specifier). Holds: the operand's spelling is the specifier, and the wholeNames fallback reads a FROM side that is no identifier as that spelling only when the marker names it with #*.
  • Whole-key predicate 2. Does not hold as written; next finding.

Findings on the ADR:

  • should-fix (A1) — whole-key predicate 2 says more than the gate reads. The note (head line 1232) reads: "none of the files the key's entry reaches is a metadata surface". The gate checks at.sourceFiles, and readExportSurface (head line 5287) keeps only files under the package's own directory (f.startsWith(mapped.dir)). The reader does reach other packages: a re-export of another workspace package's root is followed, and that package's files join the Program. So a subpath entry that writes export { SomeSchema } from '@objectstack/spec' reaches *.zod.ts files, the ADR sentence refuses PACKAGE/SUBPATH#, and the gate admits it. The same name claimed as PACKAGE/SUBPATH#SomeSchema is refused by the export form's predicate 2 (declared in a *.zod.ts), so the whole-key form admits what the per-export form refuses. The gate's reading is the defensible one: it is the twin of PACKAGE# ("none of the package's files"), and dropping the filter would refuse every key whose reach touches a spec re-export. Either way the two must agree. Smallest fix: the sentence says "none of the package's own files the key's entry reaches". The other fix, dropping the filter, changes the accept set and is the maintainer's call. No self-test pins either direction; SP-R13 drives an in-package file only. One red or green case with a cross-package re-export should come with the fix.
  • nit (A2) — the non-TS bullet's rationale, "it exports no name a TypeScript consumer imports", is true for ./style.css and false for a key whose types condition names a TS source file, which the gate refuses for the honest reason (no declaration entry to map; SP-R18). Prose face.
  • nit (A3) — "26 of 110 published packages declare subpath keys, 78 keys in all (./package.json aside)". 26 counts packages whose only subpath key is ./package.json; 24 carry one of the 78. The counts are each right; the sentence joins them loosely. Prose face.
  • nit (A4) — the 2026-10-12 addendum's opening paragraph (head line 1040) still names "three operand kinds" in root spelling and points only at the second 2026-10-27 note. A clause pointing at the 2026-11-14 note would help a reader who starts there. Outside the order's D9-only surface; an accepted deviation is fine.

scripts/check-adr-0087-registration.mjs (implementing file). The five attacks the card names:

  • A wildcard key that slips through. None found. A * in any subpath segment is refused at parse. resolveTypesEntry is a literal lookup, so a key only a pattern reaches comes back undeclared and is refused through wildcardKeysMatching, at the base and at HEAD. A literal key beside a pattern is read literally, which is Node's precedence. For the whole-key form, any matching pattern at HEAD refuses.
  • An unmappable or non-TS key not refused by name. None found. No .d.ts entry, a types entry outside dist/, a stem with no source and a stem with two sources each get their own named refusal. ./package.json falls in the first.
  • PACKAGE/SUBPATH# passing while the key is still in the HEAD map.* Not possible for an object map (hasOwnProperty on the exact key). A null or missing exports is refused. A string, an array or a conditions-only object at HEAD passes, correctly: Node exports only . for those, so the subpath is truly gone. A key whose value is null counts as still present, which over-refuses.
  • Predicate 3's spelling for the new operands. Right. The export and member forms name Export or Type.member as before; the whole-key form names the specifier; a FROM line naming the bare package, or one line per export, is out of range (SP-R14).
  • Root operands. Unchanged; see ①.

Findings on the gate, all nits (none admits a claim the contract refuses):

  • nit (G1) — wildcardKeysMatching (head line 4913) lets * match the empty string, where Node needs at least one character: ./feat* is read as reaching ./feat. Over-refusal only.
  • nit (G2) — the missing-field limb of the no-map hint is not pinned. SP-R17 drives only exports: null; a mutation of the noExportsMap test to === null would stay green. SP-R11 pins the missing field on the whole-key form only.
  • nit (G3) — the non-TS refusal says "none of its conditions names a .d.ts", which is as wide as resolveTypesEntry's walk (types, import, module, node, default, require). A .d.ts under another condition would be refused with a sentence that is false for it. No such key on main.
  • nit (G4) — the jsdoc return type of sourceEntryAt (head line 4937) lists keyAbsent and not noExportsMap.
  • nit (G5) — a subpath segment that opens with a dot (./.well-known) is refused at parse with the generic "is not a ... reference" text, not by name. No such key on main.

Implemented-by: claude/project-thread-l4k5n6
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

One should-fix (A1, the whole-key predicate 2 sentence against the gate's in-package filter) earns a delta review of its fix. The other eight findings are nits: A2 to A4 are prose faces, G1 to G5 are accepted deviations or the next touch of the gate.

Adopted by the seat 2026-11-15T00:42Z · isolated sub-agent at CONTRACT_REVIEW_TIER (served model verified by the seat) · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

ACCEPT 6886478218 withdrawn: the earlier records it rests on were served off-tier, and the at-tier review on head 92259ce2 (posted above) is PASS with one should-fix (A1: the ADR says whole-key predicate 2 checks every file the entry reaches, while the gate reads only the package's own files). The seat took pm:awaiting-merge off; a fix round and an at-tier delta follow, then a fresh hand-off. Maintainers: please do not merge this head. 2026-11-15T00:43Z

@objectstack-fleet objectstack-fleet Bot removed the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 15, 2026
D9's whole-key predicate 2 now says what the gate reads: the package's own
files the key's entry reaches, as for a whole package. A file of another
workspace package the entry re-exports is judged when its name is an export
operand. Pinned both ways (SP-G6, SP-R20), plus a missing exports field at
HEAD (SP-R17c/d). The 2026-10-12 addendum points at the new note, the non-TS
bullet names a TS-source types condition, and the sourceEntryAt docblock
names noExportsMap.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5kVi3STvGvJ9hHHAgS
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6d1f3bcee71a0530258a822160310c23e1dc1313
Local-runs: none
Scope: docs/adr/0087-metadata-protocol-upgrade-contract.md + scripts/check-adr-0087-registration.mjs

At-tier delta review. Base: 92259ce2, which holds the at-tier record 6886775383 (PASS, should-fix A1, nits A2 to A4 and G1 to G5). This head is one fix commit on that base (6d1f3bce, +47 / -14 over the same two files, no merge of main). The base record stands for everything the delta does not touch; this record judges the delta and re-reads the scope around it.

Inputs read: card #30598 (body, triage 6684725634, ruling record 6885400963, claim 6885439323, the three os-dev-reports, the ACCEPT note), PR #33520 (body, every comment including records 6886012642, 6886472906, 6886775383 and the withdrawal 6886795296, the two-file list, the net diff against main at merge base e3f0cfd4, and the delta 92259ce2..6d1f3bce), and the check-runs on this head. The developer's report delta 6887126080 was read as a claim list and not as evidence; each claim below was checked in the tree. Code outside the diff was read with git show, never reviewed: resolveTypesEntry in scripts/check-published-readme-exports.mjs. Nothing was built, tested or re-run.

Check-runs on this head: all 50 completed, 28 success, 22 skipped, none failed. Success on every lane that ran, including Lint · docs & code guards (the lint-docs-code job, which runs pnpm check:changeset-gate-self-tests and so this gate's --self-test: the SP floor of 80 has a green verdict), Lint · repo & tooling gates, Lint · scoped · tooling self-tests, Lint · PM dispatch-gates, Type Check · source gates, Test Core (1/8) to (3/8), Check Documentation Links and Governed Surface Queue Guard. Check Changeset is skipped, the skip-changeset path.

① Derived judgments

  • The accept set is unchanged by the delta. The gate hunks are a docblock, a jsdoc return type, one comment, the SP floor (73 to 80) and four self-test cases. No executable line of the gate outside selfTest() and the SELF_TEST_BATTERIES floor moved: the filter at readExportSurface (f.startsWith(mapped.dir)) and the predicate-2 branch that reads at.sourceFiles are byte-identical to the base. Right: A1 was ruled to be fixed on the ADR side, and the accept set the base record judged is the accept set here.
  • No wildcard support, still. wildcardKeysMatching is untouched and still has only refusing callers. The scope does not exceed ruling A narrowed (record 6885400963).
  • Root operands are unchanged by the delta. No root path is touched.
  • The self-test count. The delta adds seven assertions to the SP battery: SP-G6 one, SP-R20 two (red plus one pattern), SP-R17c three (red plus two patterns), SP-R17d one. 73 + 7 = 80, and 812 + 7 = 819, the developer's number. Right.
  • Public surface of the gate module: no export added or removed by the delta. Right.

② Semver level

No changeset, labelled skip-changeset; Check Changeset is skipped on this head. The diff is docs/adr/** and a repo-root scripts/ gate, which no package's files[] ships, so nothing publishes and the label is right. Clause-②: no on the PR body is right: no published schema, type or runtime surface changes. The #30511 changeset (PR #30902) is untouched and not re-graded, per the ruling.

③ Boundary flags

  • open_questions: empty in all three reports.
  • Dev deviation, the A4 edit sits in the 2026-10-12 addendum's opening paragraph, outside the claim's "D9 only" surface. Answered: the placement is right, see ④. Accepted.
  • Dev deviation, the PR body was not edited this round. The body on this head is stale against the tree: its maintainer summary and "two-minute check" still say 812 assertions and an SP battery of 73, and it does not name SP-G6, SP-R20 or SP-R17c/d. The tree says 819 and 80. The body is a prose face outside ④; the seat should apply the prepared body before the hand-off, because the maintainer's verify step would otherwise read a number the run does not print. Escalated to the seat, not a finding on the head.
  • Serial constraint from the claim (PR docs(adr-0087): the disposition text names the second no-migration-prescription refusal #33460): main now carries 406e8887 (docs(adr-0087): the disposition text names the second no-migration-prescription refusal #33460), which edits this ADR in the 2026-08-13 addendum. This head does not contain it. The hunks do not overlap anything this PR touches (D9's note, the 2026-10-12 addendum opening, the Status line, "What this does not decide"), so a merge is textually clean; but a merge of main moves the head, and this record covers 6d1f3bce only. The landing order is the seat's call.
  • Model tier deviation (the dev thread ran at the claim's default tier): this record is the at-tier review docs/adr/** owes on this head. Answered.
  • Carried from the base record and unchanged: the out-of-scope finding that a re-export of ANOTHER package's SUBPATH is not followed (resolveModuleNameLiterals asks sourceEntryAt with the whole specifier as a package name). Re-read against the new ADR sentence in ④; still conservative for the export forms.

④ Design judgment

docs/adr/0087-metadata-protocol-upgrade-contract.md (the delta: the 2026-10-12 addendum opening, the non-TS bullet, the absent-key bullet, whole-key predicates 1 and 2). Judged against the gate in this diff as its first consumer and ruling A narrowed as its execution plan.

  • A1 is fixed. Whole-key predicate 2 now reads "none of the package's own files the key's entry reaches is a metadata surface". The gate: readExportSurface returns sourceFiles as the Program's source files, mapped out of the virtual root, kept only where the path starts with mapped.dir; mapped.dir is the manifest path minus package.json, so it ends in a slash and a sibling directory with a longer name (plugin-email-x) is not taken for the package. Predicate 2 takes the first of those files metadataSurfaceKind names. The sentence and the code say the same thing, word for word: own files, reached by the entry.
  • The added sentence, clause by clause. "Another workspace package's file that the entry re-exports is that package's, as for PACKAGE#*": holds, the whole-package form lists git ls-tree under the package directory, so both forms judge by directory. "A name re-exported from it is judged when it is named as an export operand, and is refused there when it is a metadata surface": holds. The export form's predicate 2 reads declarationFiles(at, symbol) with no directory filter, so a declaration in packages/spec/src/ui/template.zod.ts is refused; SP-R20 pins that exact message. SP-G6 pins the admitted direction over the same base fixture. The pair is discriminating: SP-R20 being red on a spec path proves the spec files are in the Program, so dropping the filter would turn SP-G6 red.
  • Whole-key predicate 1 now names "exports": null. Holds: verifySubpathRetired tests map == null and words the null case apart (SP-R16, SP-R11).
  • The absent-key bullet ("names the whole-key spelling, unless the package has no exports map there, where the whole-key form is refused too"). Holds: the hint ladder reads now.keyAbsent && now.noExportsMap before now.keyAbsent, and noExportsMap is manifest.exports == null. Pinned on both limbs now (SP-R17 and R17b for null, SP-R17c and R17d for the missing field).
  • A2 is fixed. The non-TS bullet separates the two reasons: a stylesheet or JSON file exports no name, and a types condition naming a TS source names no declaration entry. The gate's refusal says the same ("a stylesheet, a JSON file or a TS SOURCE file is not one"); SP-R18 pins it.
  • A4 is fixed, and its placement is right. The 2026-10-12 addendum is the section D9 sits in, and its opening sentence is where a reader first meets the operand grammar. The sentence already carries the pointer the 2026-10-27 amendment (check-adr-0087-registration: PACKAGE_ROOTS omits ui/packages, ui/apps and ui/examples, so no @object-ui package can use the runtime-export-retired disposition #26220) added in the same parenthesis, so the check-adr-0087(runtime-export-retired): the operand names only a package's root entry, so a retired SUBPATH export (./locales/zh → ./locales/zh-Hans) can only be held as no-migration-prescription #30598 pointer follows the ADR's own precedent in the same place. It is a pointer only: it adds no rule, and the rule stays in D9's note. Leaving it out would have left the opening saying the grammar has three root-spelled operands and nothing else. The edit is outside the claim's "D9 only" line and is reported as a deviation; accepted.

Attempts to break the filter, each read in the tree:

  • A relative ../ escape (export * from '../../spec/src/ui/x.zod.js'). resolveIn normalises the path and loads the blob, so the file joins the Program under its real repo path; it does not start with the package directory and is filtered out. The whole-key form admits; the name, claimed as an export operand, is refused at predicate 2 by its declaration file. This is the reading the ADR now states.
  • A re-export through a bare specifier. A root re-export is followed and filtered as above (SP-G6 and SP-R20). A re-export of another package's SUBPATH is not resolved at all: the whole-key form admits, and the name as an export operand is refused, because declarationFiles finds no file and predicate 2 answers "cannot be verified". Refused, for a different reason than the ADR's clause gives; conservative.
  • A symlink blob. git show returns the link text, which parses to no export; judged by its own in-package path. No file of another package is reached through it.
  • A nested package.json. A directory under the package that carries its own manifest is counted as the package's own. Over-refusal only, and the same as PACKAGE#*, which lists the whole directory.
  • Conditional exports. The entry comes from resolveTypesEntry, unchanged; the filter sees only where the Program's files live.
  • A plain import of another package (not a re-export). Left unresolved by design, so nothing joins the Program. Consistent with "own files".

No case was found where the gate admits a whole-key claim the amended sentence refuses, or refuses one it admits.

scripts/check-adr-0087-registration.mjs (implementing file; the delta).

  • The rewritten predicate-2 comment says what the code under it does: sourceFiles, filtered to the package directory; the specifier limb; the re-exported name judged as an export operand. The stray backslashes are gone.
  • G4 is fixed. The sourceEntryAt docblock and its return type name noExportsMap, which subpathSourceEntry returns beside keyAbsent.
  • G2 is fixed. SP-R17c drives a HEAD manifest with the exports field missing (REX_MANIFEST({ exports: undefined }), which JSON.stringify drops) and requires the "declares no exports map at all" wording and the no-whole-key hint; SP-R17d requires that the null wording and the whole-key hint are absent. A mutation of the noExportsMap test to === null now fails SP-R17c.
  • The self-test: floor 80 for SP, 819 in all; the lane that runs it is green on this head.
  • Regression read of the rest of the scope: the delta touches no executable gate line outside the self-test, and the ADR's other check-adr-0087(runtime-export-retired): the operand names only a package's root entry, so a retired SUBPATH export (./locales/zh → ./locales/zh-Hans) can only be held as no-migration-prescription #30598 hunks (Status line, the measurement, the three marker forms, the wildcard and unmappable bullets, predicate 3, "What this does not decide") are byte-identical to the base. None.

Findings on this head:

  • nit (D1) — the docblock of verifySubpathRetired still says only "without one every file is importable by path" and does not name "exports": null, which the code under it, the ADR and SP-R16 all do. No effect; next touch of the gate.
  • Carried from the base record, untouched by the delta and still nits: A3 ("26 of 110" joins two counts loosely), G1 (* matches the empty string in wildcardKeysMatching; over-refusal), G3 (the non-TS refusal's "none of its conditions" wording), G5 (a dot-leading subpath segment refused with generic text). Accepted deviations or a follow-up card; none admits a claim the contract refuses.

Implemented-by: claude/project-thread-l4k5n6
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

A1 (should-fix) is fixed on the ADR side with the filter unchanged, and pinned in both directions. A2, A4, G2 and G4 are fixed. No must-fix and no should-fix on this head; one new nit (D1) and four carried nits (A3, G1, G3, G5) end the round. Two items for the seat outside ④: the stale PR body (812 / 73 against 819 / 80 in the tree), and main having moved on this ADR through #33460.

Adopted by the seat 2026-11-15T01:27Z · isolated sub-agent at CONTRACT_REVIEW_TIER (served model verified by the seat) · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

维护者速读

  • 改了什么:ADR-0087 门禁 runtime-export-retired 新增三种字面子路径操作数:包/子路径#导出、包/子路径#类型.成员、包/子路径#*(该键在 HEAD 的 exports 映射里已删除)。通配键、映射不到 src/ 的键、非 TS 的键都按名拒绝。ADR-0087 D9 同步原位修订,并改了「What this does not decide」一句。
  • 为什么改:执行你在决策卡上选的「A 收窄:只认字面子路径键」(卡 check-adr-0087(runtime-export-retired): the operand names only a package's root entry, so a retired SUBPATH export (./locales/zh → ./locales/zh-Hans) can only be held as no-migration-prescription #30598)。
  • 风险与回滚:只动仓库根 scripts/ 下的门禁和一篇 ADR,不发布任何包,不改任何已有操作数的判定。回滚即还原本 PR。
  • 先看:ADR-0087 D9 的 2026-11-14 修订注,以及门禁里的 verifySubpathRetired。
  • 两分钟验证:node scripts/check-adr-0087-registration.mjs --self-test,预期 819 条断言全过(SP 下限 80)。
  • 需要你做的:受管面 Tier H(docs/adr/**)。达档(CONTRACT_REVIEW_TIER,席位已核对实际模型)复审 PASS 6886775383(含 1 条应修 A1)与增量复审 PASS 评论 6887304137(Head-sha 6d1f3bcee71a0530258a822160310c23e1dc1313)已在案,请批准或直接合并。
  • 席位意见:可以合。首轮应修(HEAD 的 exports: null 会被当成「该键已删」放过)与达档复审的应修 A1(ADR 谓词 2 的措辞与门禁过滤不一致)都已改好,并有自测钉住。PR docs(adr-0087): the disposition text names the second no-migration-prescription refusal #33460 已合并,也改 ADR-0087,段落不重叠,合并 main 不冲突。

席位核对

Verdict: ACCEPT, held for the maintainer · head 6d1f3bcee71a0530258a822160310c23e1dc1313 · card #30598 · reports 6885846856, 6886302050 and 6887126080 · at-tier contract review 6886775383 (PASS with should-fix A1 on 92259ce2) and at-tier delta 6887304137 (PASS on this head, CONTRACT_REVIEW_TIER, served model verified by the seat; earlier records 6886012642 and its delta were served off-tier, correction on this PR; Ruling-ref for the in-seat review: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb option A).

  • Shape: draft → main, Fixes #30598. docs/adr/0087-metadata-protocol-upgrade-contract.md and scripts/check-adr-0087-registration.mjs. skip-changeset (no package ships either file). Clause-②: no; Metadata-protocol: none. GOVERNED, Tier H (docs/adr/**).
  • Ruling: Leehom's decision card on check-adr-0087(runtime-export-retired): the operand names only a package's root entry, so a retired SUBPATH export (./locales/zh → ./locales/zh-Hans) can only be held as no-migration-prescription #30598, option A narrowed, 「A 收窄:只认字面子路径键」. Literal keys only; a wildcard key is refused by name.
  • Should-fix closure: S1, predicate 1 for PACKAGE/SUBPATH#* now refuses a HEAD manifest whose exports is null as it refuses a missing one, and the base-side wording matches (SP-R16, red if reverted). N2, N3 and N4 are fixed with their own cases (SP-R17 to SP-R19). The merge of main (e3f0cfd4) touches neither file.
  • A1 closure: the ADR's whole-key predicate 2 now reads "the package's own files the key's entry reaches", matching readExportSurface's own-directory filter; SP-G6 and SP-R20 pin both directions. Nits A2, A4 (pointer in the 2026-10-12 addendum, accepted placement), G2 (SP-R17c/d) and G4 are folded in.
  • Accepted deviations (nits): D1, the verifySubpathRetired docblock does not name "exports": null (code, ADR and SP-R16 do); A3, G1, G3, G5 as in the at-tier record; N1, the 2026-10-12 addendum's opening paragraph does not point to the 2026-11-14 note; N5, wildcardKeysMatching lets * match the empty string and counts a null-valued literal as present (both only refuse more); F1, the ADR's non-TS bullet gives the .d.ts reason for a TS-source types key too; F2, SP-R17 pins exports: null but not a missing exports on an Export operand; F3, the non-TS message says "none of its conditions" where six are read (no key on main is affected); F5, sourceEntryAt's @returns omits noExportsMap.
  • Gates: 44 derived, 44 exit 0 on this head (pm-orders/skills/30598-gates-ran-r2.txt); self-test 819 assertions, SP floor 80. The check-runs on the head have all concluded (28 success, 22 skipped), none failed.
  • Serial: PR docs(adr-0087): the disposition text names the second no-migration-prescription refusal #33460 (card ADR-0087: the disposition section says no-migration-prescription is refused only by a body carrying a prescription; since PR #32649 it is also refused on a changeset declaring Metadata-protocol: narrowed #32674), merged, edits ADR-0087 at other hunks; it is not in this head and does not overlap. Mergeable state clean.
  • Process: the thread ran at the default tier, met by these in-seat reviews.
  • Landing: Tier H. The seat does not flip this PR ready or enqueue it. It needs the maintainer's merge, or an authorised APPROVED review after which the seat lands it.

Implemented-by: claude/project-thread-l4k5n6
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 15, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 15, 2026 01:46
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 15, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants